package edu.albany.csi518.sick;

import edu.albany.csi518.util.Query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 *
 * @author John M. Lien
 */
public class DiseaseSymptom {

    private final Query query;
    private final HashMap<String, ArrayList<String>> hashmap;
    private ArrayList<String> list_of_searched_terms;
    private SortedSet<Map.Entry<String, Integer>> rankedValues;
    private SortedSet<Map.Entry<String, Integer>> rankedValuesAll;
    private boolean debug, diseaseQuery;
    private String lastQuery;
    private int lock = 1;

    private final String queryStart = "select disease_name, symptom_name \n"
            + "	from \n"
            + "		SICK.Disease as d, \n"
            + "		SICK.Symptom as s,\n"
            + "		SICK.Disease_Symptom as r\n"
            + "	where \n"
            + "		d.disease_id = r.disease_id and \n"
            + "		s.symptom_id = r.symptom_id";

    /**
     * Take lock.
     */
    private synchronized void takeLock() {
        this.lock = 0;
    }

    /**
     * Give back lock.
     */
    private synchronized void surrenderLock() {
        this.lock = 1;
    }

    /**
     * Check Lock.
     *
     * @return Return true if lock is available;
     */
    private boolean checkLock() {
        return this.lock == 1;
    }

    /**
     *
     * @param var query from the user
     * @param debug boolean for whether debug mode is activated
     * @param diseaseQuery determines whether or not we're looking for a disease
     * or a symptom.
     */
    public synchronized boolean analyzeInput(String var, boolean debug, boolean diseaseQuery) {
        /* Take Lock */
        while (!this.checkLock()) {
            this.takeLock();
        }

        /* Set up basic variables */
        this.debug = debug;
        this.diseaseQuery = diseaseQuery;
        this.hashmap.clear();
        String queryString;
        String[] split_terms;

        /* Defend against SQL injection */
        var = var.replaceAll("[^a-zA-Z0-9 ]", "").replaceAll("[\\s+]", " ");
        if (var.isEmpty()) {
            System.out.println();
            System.out.flush();
            System.err.println("Non Valid Input");
            this.surrenderLock();
            return true;
        } else {
            if (var.length() > 300) {
                System.out.println();
                System.out.flush();
                System.err.println("Non Valid Input");
                this.surrenderLock();
                return true;
            } else {
                if (var.length() < 3) {
                    System.out.println();
                    System.out.flush();
                    System.err.println("Your input is too short. Try something a little bit longer");
                    this.surrenderLock();
                    return true;
                }
            }
        }

        /* Save serarched terms for later use */
        split_terms = var.split(" ");
        this.list_of_searched_terms = new ArrayList(Arrays.asList(split_terms));

        /* Generate a SQL query based on the input */
        queryString = this.developQuery(var);

        /* Print the query if debug mode is activated */
        if (this.debug) {
            System.out.println();
            System.out.flush();
            System.err.println("DEBUGGER:\n");
            System.err.println("---------------------------------------");
            System.err.println(queryString);
            System.err.println("---------------------------------------");
        }

        /* Populate the HashMap with data relevant to the user's interstes */
        if (this.loadHashMap(queryString)) {
            queryString = this.developQuery(split_terms);
            if (this.debug) {
                System.out.println();
                System.out.flush();
                System.err.println("DEBUGGER:\n");
                System.err.println("---------------------------------------");
                System.err.println(queryString);
                System.err.println("---------------------------------------");
            }
            /* If the query does not turn any useful results then try tokenizing the user input */
            this.loadHashMap(queryString);
        }

        /* If the user is searching via symptoms then add the rest of the symptoms */
        if (!this.diseaseQuery) {
            addMoreSymtpoms();
            rankSymptoms();
        }

        /* builds an output to be used somewhere else in the program */
        this.buildQueryOutput();

        /* Give up Lock */
        this.surrenderLock();
        return false;
    }

    /**
     * Generated a TreeMap sorted by value given the old tree map.
     *
     * @param <K> Key to Tree Map
     * @param <V> Value of Tree Map
     * @param map Tree Map
     * @return
     */
    <K, V extends Comparable<? super V>>
            SortedSet<Map.Entry<K, V>> entriesSortedByValues(Map<K, V> map) {
        SortedSet<Map.Entry<K, V>> sortedEntries = new TreeSet<>(
                new Comparator<Map.Entry<K, V>>() {
                    @Override
                    public int compare(Map.Entry<K, V> e1, Map.Entry<K, V> e2) {
                        return e2.getValue().compareTo(e1.getValue());
                    }
                }
        );
        sortedEntries.addAll(map.entrySet());
        return sortedEntries;
    }

    /**
     * Rank the the symptoms within the returned HashMap.
     */
    private void rankSymptoms() {
        int temp;
        int temp_all;
        TreeMap<String, Integer> treemap = new TreeMap();
        TreeMap<String, Integer> treemap_all = new TreeMap();
        for (String key : this.hashmap.keySet()) {
            for (String val : this.hashmap.get(key)) {
                if (treemap_all.containsKey(val)) {
                    temp_all = treemap_all.get(val);
                    treemap_all.remove(val);
                    treemap_all.put(val, ++temp_all);
                    if (!this.list_of_searched_terms.contains(val)) {
                        temp = treemap.get(val);
                        treemap.remove(val);
                        treemap.put(val, ++temp);
                    }
                } else {
                    treemap_all.put(val, 1);
                    if (!this.list_of_searched_terms.contains(val)) {
                        treemap.put(val, 1);
                    }
                }
            }
        }
        this.rankedValues = entriesSortedByValues(treemap);
        this.rankedValuesAll = entriesSortedByValues(treemap_all);
    }
       
    public Iterator<Map.Entry<String, Integer>> getRankedSymtomsIt(){
        return this.rankedValues.iterator();
    }
    
    public void printRankedSymptoms() {
        Iterator<Map.Entry<String, Integer>> iterator = this.rankedValues.iterator();
        iterator = (Iterator<Map.Entry<String, Integer>>) iterator;
        Map.Entry<String, Integer> next;
        System.err.flush();
        System.out.flush();
        for (int i = 1; iterator.hasNext(); i++) {
            next = (Map.Entry<String, Integer>) iterator.next();
            if (this.debug) {
                System.out.print(i + ".\t" + next.getKey() + "\t" + next.getValue());
                System.err.flush();
            } else {
                System.out.println(i + ".\t" + next.getKey());
                System.out.flush();
            }
        }
    }

    public Iterator<String> getIt(){
        ArrayList<String> set = new ArrayList(this.hashmap.keySet());
        
        return set.iterator();
    }
    
    public ArrayList<String> getVal(String key){
        return this.hashmap.get(key);
    }
    
    public void printDiseases() {
        ArrayList<String> set = new ArrayList(this.hashmap.keySet());
        Collections.sort(set);
        int count = 0;
        for (String key : set) {
            System.out.println(++count + ".\t" + key);
        }
    } 
    

    /**
     * Add additional symptoms to each disease if the user is searching by
     * symptom.
     */
    private void addMoreSymtpoms() {
        String symptom;
        String disease;
        this.query.executeQuery(this.queryStart);
        while (query.next()) {
            disease = query.getString("disease_name");
            if (this.hashmap.containsKey(disease)) {
                symptom = query.getString("symptom_name");
                if (!this.hashmap.get(disease).contains(symptom)) {
                    this.hashmap.get(disease).add(symptom);
                }
            }
        }
    }

    /**
     * Created the output string of results from the user input.
     */
    private void buildQueryOutput() {
        StringBuilder sb = new StringBuilder();
        sb.append("\n");
        for (String key : this.hashmap.keySet()) {
            sb.append("Disease:\t");
            sb.append(key);
            sb.append("\nSymptoms:\t");
            sb.append(this.hashmap.get(key));
            sb.append("\n\n");
        }
        this.lastQuery = sb.toString().trim();
    }

    /**
     * Gets an ArrayList of the last terms searched.
     *
     * @return ArrayList of last searched terms.
     */
    public synchronized ArrayList<String> getLastSearchTerms() {
        return this.list_of_searched_terms;
    }

    /**
     * Returns the output from the last query run by the user.
     *
     * @return Last query run by the user.
     */
    @SuppressWarnings("empty-statement")
    public synchronized String getLastQuery() {
        while (!this.checkLock());
        return this.lastQuery;
    }

    /**
     * Get the HashMap of the last query used.
     *
     * @return the query HashMap.
     */
    @SuppressWarnings("empty-statement")
    public synchronized HashMap<String, ArrayList<String>> getLastQueryHashMap() {
        while (!this.checkLock());
        return this.hashmap;
    }

    @SuppressWarnings("empty-statement")
    public synchronized SortedSet<Map.Entry<String, Integer>> getLastQueryTreeMap() {
        while (!this.checkLock());
        return this.rankedValues;
    }

    @SuppressWarnings("empty-statement")
    public synchronized SortedSet<Map.Entry<String, Integer>> getLastQueryFullTreeMap() {
        while (!this.checkLock());
        return this.rankedValuesAll;
    }

    /**
     * Loads the HashMap with data from the generated SQL query.
     *
     * @param generated_query query generated by out algorithm.
     * @return TRUE: if the HashMap is NOT empty.
     */
    private boolean loadHashMap(String generated_query) {
        query.executeQuery(generated_query);
        String disease, symptom;
        while (query.next()) {
            disease = query.getString("disease_name");
            symptom = query.getString("symptom_name");
            if (!this.hashmap.containsKey(disease)) {
                this.hashmap.put(disease, new ArrayList());
                this.hashmap.get(disease).add(symptom);
            } else {
                this.hashmap.get(disease).add(symptom);
            }
        }
        return this.hashmap.isEmpty();
    }

    /**
     * First query algorithm to be run by the program.
     *
     * @param terms Input from the user.
     * @return The generated query.
     */
    public String developQuery(String terms) {
        StringBuilder sb = new StringBuilder();
        sb.append(this.queryStart);
        sb.append(" and \n\t\t\t(");
        sb.append(this.developQueryTok(terms));
        sb.append(")");
        return sb.toString();
    }

    /**
     * Backup query algorithm if the first one fails.
     *
     * @param terms Input form the user.
     * @return The generated query.
     */
    public String developQuery(String[] terms) {
        StringBuilder sb = new StringBuilder();
        sb.append(this.queryStart);
        sb.append(" and \n\t\t\t(");
        switch (terms.length) {
            case 1:
                sb.append(this.developQueryTok(terms[0]));
                sb.append(")");
                return sb.toString();
            default:
                for (int i = 0; i < terms.length - 1; i++) {
                    sb.append(this.developQueryTok(terms[0]));
                    sb.append(" OR ");
                }
                sb.append(this.developQueryTok(terms[terms.length - 1]));
                sb.append(")");
                return sb.toString();
        }
    }

    /**
     * Helper method for contributing tokens to be appended to the SQL query..
     *
     * @param tok piece of the users input.
     * @return returns a piece of the SQL query.
     */
    public String developQueryTok(String tok) {
        return (this.diseaseQuery) ? ("disease_name like '" + tok + "%\' or disease_name like '% " + tok + "%'")
                : ("symptom_name like '" + tok + "%\' or disease_name like '% " + tok + "%'");
    }

    /**
     * Constructor for the class.
     */
    public DiseaseSymptom() {
        this.query = new Query();
        this.hashmap = new HashMap();
        this.list_of_searched_terms = new ArrayList();
    }
}
